package tech.anzhen.simple.rpc.support.rest;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
import lombok.extern.slf4j.Slf4j;
import tech.anzhen.simple.rpc.support.exception.ConnectTimeoutException;
import tech.anzhen.simple.rpc.support.exception.ReadTimeoutException;
import tech.anzhen.simple.rpc.support.exception.RemoteException;
import tech.anzhen.simple.rpc.support.logging.LoggingTracer;
import tech.anzhen.simple.rpc.support.exception.ApplicationException;
import tech.anzhen.simple.rpc.support.runtime.ModuleException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.NoConnectionReuseStrategy;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

@Slf4j
public class RestClient implements Closeable {

    private volatile CloseableHttpClient client;

    private Gson gson;

    private Charset charset = StandardCharsets.UTF_8;

    private int connectTimeout = 30000;

    private int readTimeout = 30000;

    private String defaultIdentity;

    public RestClient() {
        super();
        gson = RestUtils.buildGson();
    }

    public void setCharset(String charset) {
        this.charset = Charset.forName(charset);
    }

    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public void setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
    }

    public void setDefaultIdentity(String defaultIdentity) {
        this.defaultIdentity = defaultIdentity;
    }

    @Override
    public void close() throws IOException {
        IOUtils.closeQuietly(client);
    }

    public <T> T execute(String address, Object param, Class<T> resultType) {
        return execute(address, param, (Type) resultType);
    }

    public <T> T execute(String address, Object param, Class<T> resultType, String identity) {
        return execute(address, param, (Type) resultType, identity);
    }

    public <T> T execute(String address, Object param, Type resultType) {
        return execute(address, param, resultType, defaultIdentity);
    }


    public <T> T execute(String address, Object param, Type resultType, String identity) {

        try {
            return doExecute(address, param, resultType, identity);
        } catch (RestException e) {
            log.info("访问 " + address + " 时出现异常", e);

            throw new RemoteException(e);
        }
    }

    private <T> T doExecute(String address, Object param, Type resultType, String identity) {
        URL url;
        try {
            url = new URL(address);
        } catch (MalformedURLException e) {
            throw new RestException("wrong address: " + address, e);
        }

        RequestMessage<Object> req = new RequestMessage<Object>(param);
        String json = gson.toJson(req);
        HttpPostData postData = makePost(url, identity, json);

        CloseableHttpClient client = makeHttpClient();
        CloseableHttpResponse resp = null;
        InputStream input = null;
        try {

            log.info("对远程地址:{} 发送请求:{}", address, json);

            resp = client.execute(postData.httpPost);
            if (resp.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
                throw new RestException("http状态码不正确:" + resp.getStatusLine().getStatusCode());
            }

            input = resp.getEntity().getContent();

            return doReadResponse(address, resultType, input, resp, postData.identity);
        } catch (org.apache.http.conn.ConnectTimeoutException e) {
            throw new ConnectTimeoutException(e);
        } catch (SocketTimeoutException e) {
            throw new ReadTimeoutException(e);
        } catch (IOException e) {
            throw new RemoteException(e);
        } finally {
            IOUtils.closeQuietly(input);
            IOUtils.closeQuietly(resp);
        }
    }

    private CloseableHttpClient makeHttpClient() {
        if (client == null) {
            synchronized (this) {
                if (client == null) {
                    client = createHttpClient();
                }
            }
        }
        return client;
    }

    private CloseableHttpClient createHttpClient() {

        RequestConfig requestConfig = RequestConfig.copy(RequestConfig.DEFAULT).setConnectTimeout(this.connectTimeout)
                .setSocketTimeout(this.readTimeout).build();

        Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", RestConnectionSocketFactory.instance()).build();

        PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(registry, null, null,
                null, -1, TimeUnit.MILLISECONDS);
        poolingmgr.setDefaultMaxPerRoute(5000);
        poolingmgr.setMaxTotal(5000);

        HttpClientBuilder builder = HttpClientBuilder.create();
        builder.setConnectionTimeToLive(10, TimeUnit.SECONDS);
        builder.disableAutomaticRetries().disableRedirectHandling().disableCookieManagement();

        CloseableHttpClient client = builder.setConnectionManager(poolingmgr).setDefaultRequestConfig(requestConfig)
                .setConnectionReuseStrategy(NoConnectionReuseStrategy.INSTANCE).build();
        return client;
    }

    private HttpPostData makePost(URL url, String identity, String json) {
        HttpPost post = new HttpPost(url.toString());
        post.setHeader("Content-Type", "application/json; charset=utf-8");
        long time = System.currentTimeMillis();
        byte[] data = json.getBytes(charset);

        String tracer = LoggingTracer.get();
        if (!StringUtils.isEmpty(tracer)) {
            post.setHeader(RestUtils.X_REST_TRACER, tracer);
        }
        post.setEntity(new ByteArrayEntity(data));
        return new HttpPostData(post, identity);
    }


    private <T> T doReadResponse(String address, Type resultType, InputStream input, CloseableHttpResponse response, String identity) throws IOException {

        String json = IOUtils.toString(input, charset);
        if (StringUtils.isEmpty(json)) {
            throw new RestException("服务器返回空");
        }

        log.info("接受远程地址:{} 的结果:{}", address, StringUtils.left(json, 1000));
        ResponseMessageStub resp;
        try {
            resp = gson.fromJson(json, ResponseMessageStub.class);
        } catch (JsonSyntaxException e) {
            throw new RestException("JSON格式错误", e);
        }

        if (!resp.isSuccess()) {
            if (resp.getStack() != null) {
                log.info("访问 {} 时出现异常:code= {},moduleCode={},moduleErrorCode= {},message= {},stack= {}", address,
                        resp.getCode(), resp.getModuleCode(), resp.getModuleErrorCode(), resp.getMessage(),
                        resp.getStack());
            }

            if (RestErrorCodes.APPLICATION_ERROR.equals(resp.getCode())) {
                throw new ApplicationException(resp.getMessage());
            } else if (RestErrorCodes.MODEL_ERROR.equals(resp.getCode())) {
                throw new ModuleException(resp.getModuleErrorCode(), resp.getMessage());
            } else {
                throw new RestException(resp.getMessage(), resp.getCode());
            }

        }

        if (resultType instanceof Class) {
            if (((Class<?>) resultType).getName().equals("void")) {
                return null;
            }
        }

        return fromJson(resp.getData(), resultType);
    }

    protected <T> T fromJson(JsonElement json, Type resultType) {
        return gson.fromJson(json, resultType);
    }

    class HttpPostData {
        private HttpPost httpPost;
        private String identity;

        public HttpPostData(HttpPost httpPost, String identity) {
            this.httpPost = httpPost;
            this.identity = identity;
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println(InetAddress.getByName("www.baidu.com"));
    }

}
